package Rangel.firebrigade.buildingdetector;


import Rangel.firebrigade.buildingdetector.explore.ExploreManager;
import Rangel.firebrigade.buildingdetector.targetselector.fullygreedy.FullyGreedyTargetSelector;
import Rangel.firebrigade.buildingdetector.targetselector.IFireBrigadeTargetSelector;
import Rangel.firebrigade.world.RangelFireBrigadeWorld;
import Rangel.common.world.property.BuildingProperty;
import adf.agent.communication.MessageManager;
import adf.agent.communication.standard.bundle.MessageUtil;
import adf.agent.communication.standard.bundle.centralized.CommandPolice;
import adf.agent.communication.standard.bundle.information.MessageBuilding;
import adf.agent.communication.standard.bundle.information.MessageCivilian;
import adf.agent.develop.DevelopData;
import adf.agent.info.AgentInfo;
import adf.agent.info.ScenarioInfo;
import adf.agent.info.WorldInfo;
import adf.agent.module.ModuleManager;
import adf.component.communication.CommunicationMessage;
import adf.component.module.algorithm.PathPlanning;
import adf.component.module.complex.BuildingDetector;
import Rangel.firebrigade.cluster.Cluster;
import Rangel.firebrigade.buildingdetector.targetselector.directionbased.DirectionBasedTargetSelector;
import Rangel.common.world.tools.Util;
import Rangel.common.world.helper.RangelWorldHelper;
import Rangel.common.world.entity.RangelBuilding;
import rescuecore2.standard.entities.*;
import rescuecore2.worldmodel.EntityID;

import java.util.*;
import java.util.List;

import static rescuecore2.standard.entities.StandardEntityURN.*;

public class RangelBuildingDetector extends BuildingDetector {
    private EntityID result;

    private int sendTime;
    private int sendingAvoidTimeClearRequest;

    private Collection<EntityID> agentPositions;
    private Map<EntityID, Integer> sentTimeMap;
    private int sendingAvoidTimeReceived;
    private int sendingAvoidTimeSent;

    private int moveDistance;
    private EntityID lastPosition;
    private int positionCount;
    private Map<EntityID, BuildingProperty> sentBuildingMap;
    private TargetSelectorType targetSelectorType = TargetSelectorType.FULLY_GREEDY;
    private IFireBrigadeTargetSelector targetSelector;
    private RangelFireBrigadeWorld worldHelper;
    private PathPlanning pathPlanning;
    private ExploreManager exploreManager;

    public RangelBuildingDetector(AgentInfo ai, WorldInfo wi, ScenarioInfo si, ModuleManager moduleManager, DevelopData developData) {
        super(ai, wi, si, moduleManager, developData);
        switch (si.getMode()) {
            case PRECOMPUTATION_PHASE:
            case PRECOMPUTED:
            case NON_PRECOMPUTE:
                this.pathPlanning = moduleManager.getModule("ActionExtClear.PathPlanning", "adf.sample.module.algorithm.SamplePathPlanning");
                break;
        }

        exploreManager = moduleManager.getModule("FireBrigadeSearch.ExploreManager", "Rangel.firebrigade.buildingdetector.explore.TargetBaseExploreManager");

        this.worldHelper = (RangelFireBrigadeWorld) RangelWorldHelper.load(agentInfo, worldInfo, scenarioInfo, moduleManager, developData);
        this.sendTime = 0;
        this.sendingAvoidTimeClearRequest = developData.getInteger("SampleBuildingDetector.sendingAvoidTimeClearRequest", 5);
        this.agentPositions = new HashSet<>();
        this.sentTimeMap = new HashMap<>();
        this.sentBuildingMap = new HashMap<>();
        this.sendingAvoidTimeReceived = developData.getInteger("SampleBuildingDetector.sendingAvoidTimeReceived", 3);
        this.sendingAvoidTimeSent = developData.getInteger("SampleBuildingDetector.sendingAvoidTimeSent", 5);
        this.moveDistance = developData.getInteger("SampleBuildingDetector.moveDistance", 40000);

        registerModule(worldHelper);
        registerModule(pathPlanning);
        registerModule(exploreManager);
        setTargetSelectorApproach();
    }

    @Override
    public BuildingDetector updateInfo(MessageManager messageManager) {
        super.updateInfo(messageManager);

        if (this.getCountUpdateInfo() >= 2) {
            return this;
        }

        this.reflectMessage(messageManager);
        this.sendEntityInfo(messageManager);
        this.worldHelper.getPossibleBurningBuildings().removeAll(worldInfo.getChanged().getChangedEntities());

        worldInfo.getChanged().getChangedEntities().forEach(id -> {
            StandardEntity entity = worldInfo.getEntity(id);
            if (entity instanceof Building) {
                Building building = (Building) worldInfo.getEntity(id);
                if (building.isFierynessDefined() && building.getFieryness() > 0) {
                    BuildingProperty buildingProperty = sentBuildingMap.get(id);
                    if (buildingProperty == null || buildingProperty.getFieryness() != building.getFieryness() || buildingProperty.getFieryness() == 1) {
                        messageManager.addMessage(new MessageBuilding(true, building));
                        messageManager.addMessage(new MessageBuilding(false, building));
                        sentBuildingMap.put(id, new BuildingProperty(building));
                    }
                }
            } else if (entity instanceof Civilian) {
                Civilian civilian = (Civilian) entity;
                if ((civilian.isHPDefined() && civilian.getHP() > 1000 && civilian.isDamageDefined() && civilian.getDamage() > 0)
                        || ((civilian.isPositionDefined() && !(worldInfo.getEntity(civilian.getPosition()) instanceof Refuge))
                        && (worldInfo.getEntity(civilian.getPosition()) instanceof Building))) {
                    messageManager.addMessage(new MessageCivilian(true, civilian));
                    messageManager.addMessage(new MessageCivilian(false, civilian));
                }

            }
        });


        int currentTime = this.agentInfo.getTime();
        Human agent = (Human) this.agentInfo.me();
        int agentX = agent.getX();
        int agentY = agent.getY();
        StandardEntity positionEntity = this.worldInfo.getPosition(agent);
        if (positionEntity instanceof Road) {
            Road road = (Road) positionEntity;
            if (road.isBlockadesDefined() && road.getBlockades().size() > 0) {
                for (Blockade blockade : this.worldInfo.getBlockades(road)) {
                    if (blockade == null || !blockade.isApexesDefined()) {
                        continue;
                    }
                    if (Util.isInside(agentX, agentY, blockade.getApexes())) {
                        if ((this.sendTime + this.sendingAvoidTimeClearRequest) <= currentTime) {
                            this.sendTime = currentTime;
                            messageManager.addMessage(
                                    new CommandPolice(
                                            true,
                                            null,
                                            agent.getPosition(),
                                            CommandPolice.ACTION_CLEAR
                                    )
                            );
                            break;
                        }
                    }
                }
            }
            if (this.lastPosition != null && this.lastPosition.getValue() == road.getID().getValue()) {
                this.positionCount++;
                if (this.positionCount > this.getMaxTravelTime(road)) {
                    if ((this.sendTime + this.sendingAvoidTimeClearRequest) <= currentTime) {
                        this.sendTime = currentTime;
                        messageManager.addMessage(
                                new CommandPolice(
                                        true,
                                        null,
                                        agent.getPosition(),
                                        CommandPolice.ACTION_CLEAR
                                )
                        );
                    }
                }
            } else {
                this.lastPosition = road.getID();
                this.positionCount = 0;
            }
        }
        return this;
    }

    @Override
    public BuildingDetector calc() {
        try {
            Cluster targetCluster;
            targetCluster = worldHelper.getFireClustering().findNearestCluster((worldHelper.getSelfLocation()));
            if (targetCluster != null) {
                if (!targetCluster.isControllable() && !targetSelectorType.equals(TargetSelectorType.DIRECTION_BASED14)) {
                    targetSelectorType = TargetSelectorType.DIRECTION_BASED14;
                    setTargetSelectorApproach();
                } else if (targetCluster.isControllable() && !targetSelectorType.equals(TargetSelectorType.FULLY_GREEDY)) {
                    targetSelectorType = TargetSelectorType.FULLY_GREEDY;
                    setTargetSelectorApproach();
                }
            }

            if (this.worldHelper.getFireClustering().getClusterNumber() > 0) {
                double minDistance = Double.MAX_VALUE;
                for (int i = 0; i < this.worldHelper.getFireClustering().getClusterConvexPolygons().size(); i++) {
                    double distance = Util.distance(this.worldHelper.getFireClustering().getClusterConvexPolygons().get(i), worldInfo.getLocation(agentInfo.getID()), false);
                    if (distance < minDistance) {
                        minDistance = distance;
                    }
                }

                FireBrigadeTarget fireBrigadeTarget = targetSelector.selectTarget(targetCluster);//

                RangelBuilding lastSelectedBuilding;
                if (fireBrigadeTarget != null) {
                    lastSelectedBuilding = fireBrigadeTarget.getRangelBuilding();
                    findPossibleBurningBuildings(lastSelectedBuilding);
                } else {
                    lastSelectedBuilding = null;
                }
                if (exploreManager.isTimeToExplore(fireBrigadeTarget)) {
                    EntityID exploreTarget;
                    exploreManager.calc();
                    exploreTarget = exploreManager.getTarget();
                    worldHelper.setExploreTarget(exploreTarget);
                    if (exploreTarget != null) {
                        lastSelectedBuilding = null;
                    }
                } else {
                    worldHelper.setExploreTarget(null);
                }
                if (lastSelectedBuilding != null) {
                    this.result = lastSelectedBuilding.getID();
                } else {
                    this.result = null;
                }

            } else {
                this.result = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return this;
    }

    private void findPossibleBurningBuildings(RangelBuilding lastSelectedBuilding) {

        if (lastSelectedBuilding != null) {
            List<RangelBuilding> objectsInRange = lastSelectedBuilding.getConnectedBuilding();
            for (RangelBuilding RangelBuilding : objectsInRange) {
                if (!RangelBuilding.getSelfBuilding().isOnFire() && worldInfo.getDistance(lastSelectedBuilding.getID(), RangelBuilding.getID()) < scenarioInfo.getPerceptionLosMaxDistance()) {
                    worldHelper.getPossibleBurningBuildings().add(RangelBuilding.getID());
                }
            }
        }
    }


    private void setTargetSelectorApproach() {//两种不同类型的目标选择的方法

        switch (targetSelectorType) {
            case FULLY_GREEDY:
                targetSelector = new FullyGreedyTargetSelector(worldHelper, agentInfo);
                break;
            case DIRECTION_BASED14:
                targetSelector = new DirectionBasedTargetSelector(worldHelper, agentInfo);
                break;

        }
    }

    @Override
    public EntityID getTarget() {
        return this.result;
    }

    private void reflectMessage(MessageManager messageManager) {
        Set<EntityID> changedEntities = this.worldInfo.getChanged().getChangedEntities();
        changedEntities.add(this.agentInfo.getID());
        int time = this.agentInfo.getTime();
        for (CommunicationMessage message : messageManager.getReceivedMessageList(MessageBuilding.class)) {
            MessageBuilding mb = (MessageBuilding) message;
            if (!changedEntities.contains(mb.getBuildingID())) {
                MessageUtil.reflectMessage(this.worldInfo, mb);
            }
            this.sentTimeMap.put(mb.getBuildingID(), time + this.sendingAvoidTimeReceived);
        }

        for (CommunicationMessage message : messageManager.getReceivedMessageList(MessageCivilian.class)) {
            MessageCivilian mc = (MessageCivilian) message;
            if (!changedEntities.contains(mc.getAgentID())) {
                MessageUtil.reflectMessage(this.worldInfo, mc);
            }
        }
    }

    private boolean checkSendFlags() {
        boolean isSendBuildingMessage = true;

        StandardEntity me = this.agentInfo.me();
        if (!(me instanceof Human)) {
            return false;
        }
        Human agent = (Human) me;
        EntityID agentID = agent.getID();
        EntityID position = agent.getPosition();
        StandardEntityURN agentURN = agent.getStandardURN();
        EnumSet<StandardEntityURN> agentTypes = EnumSet.of(AMBULANCE_TEAM, FIRE_BRIGADE, POLICE_FORCE);
        agentTypes.remove(agentURN);

        this.agentPositions.clear();
        for (StandardEntity entity : this.worldInfo.getEntitiesOfType(agentURN)) {
            Human other = (Human) entity;
            if (isSendBuildingMessage) {
                if (other.getPosition().getValue() == position.getValue()) {
                    if (other.getID().getValue() > agentID.getValue()) {
                        isSendBuildingMessage = false;
                    }
                }
            }
            this.agentPositions.add(other.getPosition());
        }
        for (StandardEntityURN urn : agentTypes) {
            for (StandardEntity entity : this.worldInfo.getEntitiesOfType(urn)) {
                Human other = (Human) entity;
                if (isSendBuildingMessage) {
                    if (other.getPosition().getValue() == position.getValue()) {
                        if (urn == FIRE_BRIGADE) {
                            isSendBuildingMessage = false;
                        } else if (agentURN != FIRE_BRIGADE && other.getID().getValue() > agentID.getValue()) {
                            isSendBuildingMessage = false;
                        }
                    }
                }
                this.agentPositions.add(other.getPosition());
            }
        }
        return isSendBuildingMessage;
    }

    private void sendEntityInfo(MessageManager messageManager) {
        if (this.checkSendFlags()) {
            Building building = null;
            int currentTime = this.agentInfo.getTime();
            Human agent = (Human) this.agentInfo.me();
            for (EntityID id : this.worldInfo.getChanged().getChangedEntities()) {
                StandardEntity entity = this.worldInfo.getEntity(id);
                if (entity instanceof Building) {
                    Integer time = this.sentTimeMap.get(id);
                    if (time != null && time > currentTime) {
                        continue;
                    }
                    Building target = (Building) entity;
                    if (!this.agentPositions.contains(target.getID())) {
                        building = this.selectBuilding(building, target);
                    } else if (target.getID().getValue() == agent.getPosition().getValue()) {
                        building = this.selectBuilding(building, target);
                    }
                }
            }
            if (building != null) {
                messageManager.addMessage(new MessageBuilding(true, building));
                this.sentTimeMap.put(building.getID(), currentTime + this.sendingAvoidTimeSent);
            }
        }
    }

    private Building selectBuilding(Building building1, Building building2) {
        if (building1 != null) {
            if (building2 != null) {
                if (building1.isOnFire() && building2.isOnFire()) {
                    if (building1.getFieryness() < building2.getFieryness()) {
                        return building2;
                    } else if (building1.getFieryness() > building2.getFieryness()) {
                        return building1;
                    }
                    if (building1.isTemperatureDefined() && building2.isTemperatureDefined()) {
                        return building1.getTemperature() < building2.getTemperature() ? building2 : building1;
                    }
                } else if (!building1.isOnFire() && building2.isOnFire()) {
                    return building2;
                }
            }
            return building1;
        }
        return building2;
    }

    private int getMaxTravelTime(Area area) {
        int distance = 0;
        List<Edge> edges = new ArrayList<>();
        for (Edge edge : area.getEdges()) {
            if (edge.isPassable()) {
                edges.add(edge);
            }
        }
        if (edges.size() <= 1) {
            return Integer.MAX_VALUE;
        }
        for (int i = 0; i < edges.size(); i++) {
            for (int j = 0; j < edges.size(); j++) {
                if (i != j) {
                    Edge edge1 = edges.get(i);
                    double midX1 = (edge1.getStartX() + edge1.getEndX()) >> 1;
                    double midY1 = (edge1.getStartY() + edge1.getEndY()) >> 1;
                    Edge edge2 = edges.get(j);
                    double midX2 = (edge2.getStartX() + edge2.getEndX()) >> 1;
                    double midY2 = (edge2.getStartY() + edge2.getEndY()) >> 1;
                    int d = this.getDistance(midX1, midY1, midX2, midY2);
                    if (distance < d) {
                        distance = d;
                    }
                }
            }
        }
        if (distance > 0) {
            return (distance / this.moveDistance) + ((distance % this.moveDistance) > 0 ? 1 : 0) + 1;
        }
        return Integer.MAX_VALUE;
    }

    private int getDistance(double fromX, double fromY, double toX, double toY) {
        double dx = toX - fromX;
        double dy = toY - fromY;
        return (int) Math.hypot(dx, dy);
    }
}
